﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Skugo.Core;
using Skugo.Shared;
using System.IO;
using AvalonDock;
using ICSharpCode.AvalonEdit.CodeCompletion;
using System.Windows.Input;
using System.Xml;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
using ICSharpCode.AvalonEdit.Highlighting;
using System.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Document;
using System.Threading;
using System.ComponentModel;

namespace Skugo.StandardPlugins
{
    public class TextEditor : DockablePlugin
    {
        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        enum MessageType : byte
        {
            SetText,
            SetSyntaxHighlighting,
            SetMemberAccessTrigger,
            SetCallTrigger,
            ShowAutoComplete,
            ShowAutoCompleteUsingDocumentWords,
            ShowCallTips,
            TextChanged,
            TextEntered,
            TriggerMemberAccess,
            TriggerCall,
        }

        private TextEditorForm Form;
        private CompletionWindow Completion;
        private Int32 CompletionStart = -1;
        private Int32 CompletionEnd = -1;
        private String CompletionPartialIdentifier = null;
        private OverloadInsightWindow Overloads;
        private Char MemberAccessTrigger = '.';
        private Char CallTrigger = '(';
        private HashSet<String> DocumentWords = new HashSet<String>();

        protected override void Initialize(BinaryReader reader)
        {
            base.Initialize(reader);

            this.Form = new TextEditorForm();
            this.Content = this.Form;

            this.Form.Editor.TextChanged += this.OnTextChanged;
            this.Form.Editor.TextArea.TextEntered += this.OnTextEntered;
            this.Form.Editor.PreviewKeyDown += new KeyEventHandler(Editor_PreviewKeyDown);
        }

        void Editor_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            // If we're pressing the 'auto-complete' key...
            if (e.Key == System.Windows.Input.Key.Space && e.KeyboardDevice.Modifiers == ModifierKeys.Control)
            {
                this.TriggerAutoComplete(String.Empty, this.MemberAccessTrigger, true);
                e.Handled = true;
            }
        }

        private void CloseSuggestionWindows()
        {
            if (this.Overloads != null)
            {
                this.Overloads.Close();
                this.Overloads = null;
            }
            if (this.Completion != null)
            {
                this.Completion.Close();
                this.Completion = null;
            }
        }

        void OnTextEntered(object sender, TextCompositionEventArgs e)
        {
            // Special case where we should close the overload window
            if (e.Text == ")" || e.Text == "]")
            {
                this.CloseSuggestionWindows();
            }

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.TextEntered);
            outgoing.WriteAsciiString(e.Text);
            this.Send(stream.ToArray());

            // If we pressed the auto complete trigger character
            if (e.Text.Length == 1)
            {
                // Get the typed character
                var triggerChar = e.Text[0];

                // If the character we just typed was the auto complete trigger character
                if (triggerChar == this.MemberAccessTrigger || triggerChar == this.CallTrigger)
                {
                    this.TriggerAutoComplete(e.Text, triggerChar, false);
                }
            }
        }

        private void TriggerAutoComplete(String textTyped, Char trigger, Boolean isCtrlSpace)
        {
            var identifiersBefore = new List<String>();

            // Reset the place where the completion is taking place
            this.CompletionStart = -1;
            this.CompletionEnd = -1;
            this.CompletionPartialIdentifier = null;

            // As long as our cursor is in front of something...
            if (this.Form.Editor.SelectionStart > 0)
            {
                // Grab the current text from the editor
                var text = this.Form.Editor.Text;

                // The end of our string range always starts one before the selection
                // In other words, the character before the cursor | (always valid since the above check)
                var end = this.Form.Editor.SelectionStart - 1;
                var start = end;

                // We start at the end character and walk this index backwards
                var index = end;

                bool started = false;
                bool isPartialIdentifier = true;

                for (; ; )
                {
                    // This special case helps out our algorhtm for when we reach the start
                    // Instead of having to do the below 'add identifier' code twice, we just
                    // make believe there is a null character at the index of -1
                    var character = '\0';
                    if (index >= 0)
                    {
                        // Read the current character (should always be valid)
                        character = text[index];
                    }

                    // If the current character is a valid identifier character...
                    if (Char.IsLetterOrDigit(character))
                    {
                        // If we didn't start reading an identifier yet, start here
                        if (started == false)
                        {
                            started = true;
                            end = index;
                        }
                        
                        // We always set the start of the identifier to right here since it's valid
                        // The start will gradually move backwards until we reach a non identifier character
                        start = index;
                    }
                    // Otherwise, we hit a character that is not part of an identifier
                    else
                    {
                        // We need to end the current identifier (if we started one)
                        if (started)
                        {
                            // We add one (1) since the end can be the start, but we
                            // literally want that to be 1 character
                            var length = end - start + 1;

                            // Get the identifier and add it to the list
                            var identifier = text.Substring(start, length);
                            identifiersBefore.Add(identifier);

                            // As a special case, if we're pressing Ctrl+Space and we have a partial identifier...
                            if (isPartialIdentifier && isCtrlSpace)
                            {
                                // We need to select that partial identifier
                                //this.Form.Editor.SelectionStart = start;
                                //this.Form.Editor.SelectionLength = length;
                                this.CompletionStart = start;
                                this.CompletionEnd = start + length;
                                this.CompletionPartialIdentifier = identifier;
                            }

                            // We are no longer reading an identifier
                            started = false;
                        }
                        
                        // No matter what, once we hit this point we're not parsing our fisrt partial identifier
                        isPartialIdentifier = false;

                        // If the character we read is the auto-complete character
                        if (character != this.MemberAccessTrigger && character != this.CallTrigger)
                        {
                            break;
                        }
                    }

                    --index;
                }
            }

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Get the line that the trigger occurred on
            var line = (Int32)this.Form.Editor.Document.GetLineByOffset(this.Form.Editor.SelectionStart).LineNumber;

            // Write out a the frame buffer message
            if (trigger == this.MemberAccessTrigger)
            {
                outgoing.Write((Byte)MessageType.TriggerMemberAccess);
            }
            else
            {
                outgoing.Write((Byte)MessageType.TriggerCall);
            }

            outgoing.Write(line);
            outgoing.Write(isCtrlSpace);
            outgoing.Write((Int32)identifiersBefore.Count);
            foreach (var identifier in identifiersBefore.Reverse<String>())
            {
                outgoing.WriteAsciiString(identifier);
            }
            this.Send(stream.ToArray());
        }

        void ParseWords(Object textContext)
        {
            var text = (String)textContext;
            var count = text.Length;

            var words = new HashSet<String>();

            var start = 0;
            var started = false;

            for (var i = 0; i <= count; ++i)
            {
                // Get the character at i, or if we're at the end return a null
                var c = (i != count) ? text[i] : '\0';

                // We only start new words on letters
                if (Char.IsLetter(c))
                {
                    // If we haven't started a word and we hit a letter, start a new word here
                    if (!started)
                    {
                        start = i;
                        started = true;
                    }
                }
                // However, the word can contain digits
                else if (Char.IsDigit(c) == false)
                {
                    // If we started a word and we're not a digit, then chop this current word off
                    if (started)
                    {
                        started = false;
                        var word = text.Substring(start, i - start);
                        words.Add(word);
                    }
                }
            }

            this.DocumentWords = words;
        }

        void OnTextChanged(object sender, EventArgs e)
        {
            this.ParseWords(this.Form.Editor.Text);

            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the frame buffer message
            outgoing.Write((Byte)MessageType.TextChanged);
            outgoing.WriteAsciiString(this.Form.Editor.Text);
            this.Send(stream.ToArray());
        }

        protected override void OnReceive(byte[] data)
        {
            var stream = new MemoryStream(data);

            var incoming = new BinaryReader(stream);

            var message = (MessageType)incoming.ReadByte();

            switch (message)
            {
                case MessageType.SetText:
                {
                    var text = incoming.ReadAsciiString();
                    this.Form.Editor.Text = text;
                    break;
                }
                case MessageType.SetSyntaxHighlighting:
                {
                    var text = incoming.ReadAsciiString();

                    try
                    {
                        var textReader = new StringReader(text);
                        using (XmlReader reader = new XmlTextReader(textReader))
                        {
                            var customHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
                            this.Form.Editor.SyntaxHighlighting = customHighlighting;
                        }
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show("Unable to set syntax highlighting:\n" + e.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                    }
                    break;
                }
                case MessageType.SetMemberAccessTrigger:
                {
                    var character = incoming.ReadChar();
                    this.MemberAccessTrigger = character;
                    break;
                }
                case MessageType.SetCallTrigger:
                {
                    var character = incoming.ReadChar();
                    this.CallTrigger = character;
                    break;
                }
                case MessageType.ShowAutoComplete:
                {
                    this.CloseSuggestionWindows();

                    var entries = incoming.ReadInt32();

                    if (entries == 0)
                    {
                        break;
                    }

                    this.Completion = new CompletionWindow(this.Form.Editor.TextArea);
                    IList<ICompletionData> list = this.Completion.CompletionList.CompletionData;

                    for (var i = 0; i < entries; ++i)
                    {
                        var entry = incoming.ReadAsciiString();
                        var description = incoming.ReadAsciiString();
                        var icon = (Icon)incoming.ReadInt32();
                        list.Add(new EditorCompletionData(entry, description, icon));
                    }

                    if (this.CompletionPartialIdentifier != null)
                    {
                        this.Completion.StartOffset = this.CompletionStart;
                        this.Completion.EndOffset = this.CompletionEnd;
                        this.Completion.CompletionList.SelectItem(this.CompletionPartialIdentifier);
                    }

                    this.Completion.Closed += delegate
                    {
                        this.Completion = null;
                    };

                    this.Completion.Show();
                    break;
                }
                case MessageType.ShowAutoCompleteUsingDocumentWords:
                {
                    this.CloseSuggestionWindows();

                    var words = this.DocumentWords;

                    if (words.Count == 0)
                    {
                        break;
                    }

                    this.Completion = new CompletionWindow(this.Form.Editor.TextArea);
                    IList<ICompletionData> list = this.Completion.CompletionList.CompletionData;

                    if (this.CompletionStart != -1)
                    {
                        this.Completion.StartOffset = this.CompletionStart;
                        this.Completion.EndOffset = this.CompletionEnd;
                    }

                    foreach (var word in words)
                    {
                        list.Add(new EditorCompletionData(word, "", Icon.PageOrLayer));
                    }

                    this.Completion.Closed += delegate
                    {
                        this.Completion = null;
                    };

                    this.Completion.Show();
                    break;
                }
                case MessageType.ShowCallTips:
                {
                    this.CloseSuggestionWindows();

                    var entries = incoming.ReadInt32();

                    if (entries == 0)
                    {
                        break;
                    }

                    var overloads = new List<Overload>();

                    for (var i = 0; i < entries; ++i)
                    {
                        var signature = incoming.ReadAsciiString();
                        var description = incoming.ReadAsciiString();
                        overloads.Add(new Overload()
                        {
                            Signature = signature,
                            Description = description,
                        });
                    }

                    this.Overloads = new OverloadInsightWindow(this.Form.Editor.TextArea);
                    this.Overloads.Provider = new MethodOverloads(overloads, this.Overloads);


                    this.Overloads.Closed += delegate
                    {
                        this.Overloads = null;
                    };

                    this.Overloads.Show();
                    break;
                }
            }
        }
    }

    public class Overload
    {
        public String Signature;
        public String Description;
    }

    public class MethodOverloads : IOverloadProvider
    {
        private OverloadInsightWindow Window;

        public MethodOverloads(List<Overload> overloads, OverloadInsightWindow window)
        {
            this.Window = window;
            this.Overloads = overloads;
        }

        private List<Overload> Overloads;

        public int Count
        {
            get { return this.Overloads.Count; }
        }

        public object CurrentContent
        {
            get { return this.Overloads[this.SelectedIndex].Description; }
        }

        public object CurrentHeader
        {
            get { return this.Overloads[this.SelectedIndex].Signature; }
        }

        public string CurrentIndexText
        {
            get { return (this.SelectedIndex + 1).ToString() + " of " + this.Count.ToString(); }
        }

        public int SelectedIndex
        {
            get { return this.InternalSelectedIndex; }
            set
            {
                this.InternalSelectedIndex = value;

                Framework.Main.Title = this.CurrentIndexText;
                this.OnPropertyChanged("SelectedIndex");
                this.OnPropertyChanged("CurrentContent");
                this.OnPropertyChanged("CurrentHeader");
                this.OnPropertyChanged("CurrentIndexText");
            }
        }

        private int InternalSelectedIndex;

        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public class EditorCompletionData : ICompletionData
    {
        public EditorCompletionData(String text, String description, Icon icon)
        {
            this.Text = text;
            this.Icon = icon;

            if (description == null || description.Length == 0)
            {
                this.Description = "Auto-complete with '" + text + "'";
            }
            else
            {
                this.Description = description;
            }
        }

        private Icon Icon;

        public ImageSource Image
        {
            get
            {
                return IconLoader.Load(this.Icon);
            }
        }

        public string Text { get; private set; }

        public object Content
        {
            get { return this.Text; }
        }

        public object Description { get; private set; }


        public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs)
        {
            textArea.Document.Replace(completionSegment, this.Text);
        }

        public double Priority
        {
            get { return 0.0; }
        }
    }
}
